sartUP — UI/UX Spec (Contenitore unico **Admin**, Login esterno, Menu dinamico)

sartUP — UI/UX Spec (Contenitore unico Admin, Login esterno, Menu dinamico)

> Decisione: un solo contenitore Admin/ per tutta l’area autenticata. Il menù è gestito da DB e filtrato per ruolo. > Il menù di servizio (solo super-admin) ha come prima voce: Configurazione menù. > Login esterno all’applicazione con layout 1/3–2/3.

---

0) Struttura di riferimento (menu → cartelle)

L’architettura del codice ricalca la gerarchia del menù:
  • Topbar (L1): macro–sezioni (es. Dashboard, Industria 4.0, Servizio).
  • Sidebar (L2/L3/L4): sottosezioni figlie della voce L1 attiva.
  • Cartelle parlanti: controllers/views organizzati come il menù.
  • `` app/Http/Controllers/Admin/ Dashboard/DashboardController.php I40/HomeController.php I40/Machines/MachinesController.php Systems/ # “Servizio” (solo super-admin) Menu/MenuController.php Users/UsersController.php (placeholder) Roles/RolesController.php (placeholder)

    app/Services/MenuService.php app/Http/Middleware/EnsureActiveRole.php app/Policies/MenuPolicy.php (opzionale) app/Models/Menu.php app/Models/MenuItem.php

    resources/views/layouts/admin.blade.php resources/views/layouts/auth.blade.php # login esterno (1/3–2/3) resources/views/auth/{login,select-role,forgot,reset}.blade.php

    resources/views/admin/dashboard/index.blade.php resources/views/admin/i40/home.blade.php resources/views/admin/i40/machines/connected.blade.php resources/views/admin/systems/index.blade.php resources/views/admin/systems/menu/{index,create,edit}.blade.php

    routes/auth.php routes/admin.php `

    Nota ruoli: l’area Admin/ è per tutti i ruoli autenticati (operator, maintenance, admin, super-admin). Le voci/azioni sono limitate da RBAC. “Servizio” è visibile solo al ruolo super-admin e contiene la Configurazione menù (CRUD).

    ---

    1) Login esterno (pagina pubblica)

  • Percorso: /login (guest).
  • Layout dedicato: resources/views/layouts/auth.blade.php (nessuna topbar/sidebar).
  • Struttura 1/3–2/3:
  • - Sinistra (1/3): logo grande (circolare consigliato), nome, descrizione, immagine. - Destra (2/3): form credenziali + “remember me” + “forgot password”.

    1.1 Layout Blade (auth)

    `blade {{-- resources/views/layouts/auth.blade.php --}} <!doctype html> <html lang="it"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width,initial-scale=1"/> <title>@yield('title','Accedi | sartUP')</title> @vite(['resources/css/app.css','resources/js/app.js']) </head> <body class="min-h-screen bg-gray-50"> <div class="mx-auto max-w-6xl min-h-screen grid grid-cols-3"> {{-- Colonna sinistra 1/3: branding --}} <aside class="col-span-1 bg-white border-r p-8 flex flex-col justify-center"> <div class="flex items-center gap-3 mb-6"> <img src="/images/logo.png" alt="sartUP" class="h-12 w-12 rounded-full object-cover"> <div> <h1 class="text-xl font-semibold">sartUP</h1> <p class="text-sm text-gray-500">Gestionale modulare per confezione</p> </div> </div> <div class="text-gray-600 text-sm leading-relaxed"> <p>Accedi per gestire modelli, tempi & metodi, integrazioni Industria 4.0 e reportistica.</p> </div> {{-- <img src="/images/login-side.jpg" class="mt-8 rounded-xl shadow" alt=""> --}} </aside>

    {{-- Colonna destra 2/3: form --}} <main class="col-span-2 flex items-center justify-center p-8"> <div class="w-full max-w-md bg-white rounded-xl shadow p-6"> @yield('content') </div> </main> </div> </body> </html> `

    1.2 View Login (form + forgot)

    `blade {{-- resources/views/auth/login.blade.php --}} @extends('layouts.auth') @section('title','Accedi') @section('content') <h2 class="text-lg font-semibold mb-4">Accedi</h2> <form method="POST" action="{{ route('login.post') }}" class="space-y-4"> @csrf <div> <label class="block text-sm mb-1">Email</label> <input type="email" name="email" value="{{ old('email') }}" required autofocus class="w-full border rounded p-2"> @error('email') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror </div> <div> <label class="block text-sm mb-1">Password</label> <input type="password" name="password" required class="w-full border rounded p-2"> @error('password') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror </div> <div class="flex items-center justify-between"> <label class="inline-flex items-center gap-2 text-sm"> <input type="checkbox" name="remember" class="rounded"> Ricordami </label> <a class="text-sm underline" href="{{ route('password.request') }}">Hai dimenticato la password?</a> </div> <button class="w-full py-2 rounded bg-blue-600 text-white font-medium">Entra</button> </form> @endsection `

    > Dopo l’autenticazione: redirect all’Admin template (/admin). Se l’utente ha >1 ruolo → pagina select-role.

    ---

    2) Admin template: Topbar, Sidebar 3 livelli, Breadcrumb + Titolo + Body

    2.1 Topbar (richieste)

  • Sinistra: logo piccolo circolare + menù principale orizzontale (Livello 1).
  • Destra: profilo utente (avatar iniziale + dropdown: “Cambia ruolo”, “Profilo”, “Logout”).
  • 2.2 Body (richieste)

  • Due sezioni:
  • - Sidebar sinistra: menù fino a 3 livelli (L2/L3/L4) in base alla voce L1 attiva. - Corpo centrale: 1) Barra breadcrumb + Titolo modulo (coerenti con il ramo corrente del menù). 2) Body del modulo (contenuto della view).

    2.3 Layout Blade (admin)

    `blade {{-- resources/views/layouts/admin.blade.php --}} @php / @var \App\Services\MenuService $ms */ $ms = app(\App\Services\MenuService::class); $menuL1 = $ms->forUserMenu('admin_main', auth()->user()); // topbar $active = $ms->currentBranchByRoute('admin_main'); // item corrente + ancestors $sidebarTree = $ms->childrenForTopLevel($active['top'] ?? $menuL1[0] ?? null); // L2/L3/L4 @endphp <!doctype html> <html lang="it"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width,initial-scale=1"/> <title>@yield('title','sartUP Admin')</title> @vite(['resources/css/app.css','resources/js/app.js']) </head> <body class="min-h-screen bg-gray-50"> {{-- TOPBAR --}} <header class="h-14 shadow flex items-center px-4 bg-white"> <div class="flex items-center gap-3 mr-6"> <img src="/images/logo.png" class="h-8 w-8 rounded-full object-cover" alt="logo"> </div> <nav class="flex gap-4"> @foreach($menuL1 as $item) <a href="{{ $item['route_name'] ? route($item['route_name']) : ($item['url'] ?? '#') }}" class="font-medium {{ request()->routeIs(($item['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}"> {{ $item['label'] }} </a> @endforeach @role('super-admin') <a href="{{ route('admin.systems.menu.index') }}" class="font-medium {{ request()->is('admin/systems*') ? 'text-blue-600' : '' }}">Servizio</a> @endrole </nav> <div class="ml-auto flex items-center gap-3"> @if(session('active_role')) <span class="text-xs px-2 py-1 bg-gray-200 rounded">Ruolo: {{ session('active_role') }}</span> @endif <div class="relative"> <details> <summary class="list-none flex items-center gap-2 cursor-pointer"> <div class="h-8 w-8 rounded-full bg-gray-200 flex items-center justify-center"> {{ strtoupper(substr(auth()->user()->name ?? 'U',0,1)) }} </div> <span class="text-sm">{{ auth()->user()->name }}</span> </summary> <div class="absolute right-0 mt-2 bg-white border rounded shadow min-w-[200px]"> <a class="block px-3 py-2 hover:bg-gray-50" href="{{ route('auth.role.select') }}">Cambia ruolo</a> <a class="block px-3 py-2 hover:bg-gray-50" href="#">Profilo</a> <form method="POST" action="{{ route('logout') }}" class="border-t"> @csrf <button class="w-full text-left px-3 py-2 hover:bg-gray-50">Logout</button> </form> </div> </details> </div> </div> </header>

    <div class="flex"> {{-- SIDEBAR: L2/L3/L4 --}} <aside class="w-72 bg-white border-r min-h-[calc(100vh-3.5rem)] p-4"> @if(!empty($sidebarTree)) @foreach($sidebarTree as $l2) <div class="mb-3"> <div class="text-sm font-semibold mb-1">{{ $l2['label'] }}</div> @if(!empty($l2['children'])) <ul class="ml-3 space-y-1"> @foreach($l2['children'] as $l3) <li> <a class="block text-sm {{ request()->routeIs(($l3['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}" href="{{ $l3['route_name'] ? route($l3['route_name']) : ($l3['url'] ?? '#') }}"> {{ $l3['label'] }} </a> @if(!empty($l3['children'])) <ul class="ml-4 mt-1 space-y-1"> @foreach($l3['children'] as $l4) <li> <a class="block text-sm {{ request()->routeIs(($l4['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}" href="{{ $l4['route_name'] ? route($l4['route_name']) : ($l4['url'] ?? '#') }}"> {{ $l4['label'] }} </a> </li> @endforeach </ul> @endif </li> @endforeach </ul> @endif </div> @endforeach @endif </aside>

    {{-- MAIN: breadcrumb + titolo + body --}} <main class="flex-1 p-6"> <div class="mb-4"> <nav class="text-sm text-gray-500"> @if(!empty($active['trail'])) @foreach($active['trail'] as $i => $crumb) @if($i>0) <span class="mx-1">/</span> @endif @if($crumb['route_name']) <a class="hover:underline" href="{{ route($crumb['route_name']) }}">{{ $crumb['label'] }}</a> @else <span>{{ $crumb['label'] }}</span> @endif @endforeach @endif </nav> <h1 class="text-xl font-semibold mt-1">@yield('title','Modulo')</h1> </div> @yield('content') </main> </div> </body> </html> `

    ---

    3) Service: branch attivo, sidebar e topbar

    3.1 Metodi aggiuntivi in MenuService

    `php // App/Services/MenuService.php (estratti/aggiunte) public function currentBranchByRoute(string $menuName = 'admin_main'): array { $menu = Menu::where('name',$menuName)->first(); if (!$menu) return ['trail'=>[], 'top'=>null]; $items = $menu->items()->get(); $byId = $items->keyBy('id'); $current = $items->first(function($i){ return $i->route_name && request()->routeIs($i->route_name.'*'); }); if (!$current) return ['trail'=>[], 'top'=>null];

    $trail = []; while ($current) { $trail[] = ['label'=>$current->label,'route_name'=>$current->route_name,'id'=>$current->id,'parent_id'=>$current->parent_id]; $current = $current->parent_id ? $byId->get($current->parent_id) : null; } $trail = array_reverse($trail); $top = $trail[0] ?? null; return ['trail'=>$trail, 'top'=>$top]; }

    public function childrenForTopLevel(?array $top): array { if (!$top) return []; $menu = Menu::where('name','admin_main')->first(); if (!$menu) return []; $items = $menu->items()->orderBy('order_index')->get()->groupBy('parent_id'); $build = function($parentId) use (&$build,$items) { return ($items[$parentId] ?? collect())->map(fn($i)=> ['id'=>$i->id,'label'=>$i->label,'route_name'=>$i->route_name,'url'=>$i->url,'children'=>$build($i->id)->values()->all()] ); }; return $build($top['id'] ?? null)->values()->all(); } `

    > Il filtraggio per ruolo/permessi è già presente nel metodo principale che costruisce L1. Per coerenza, puoi riapplicarlo anche alla sidebar (stessa closure filter).

    ---

    4) Rotte & accessi

    4.1 RouteServiceProvider

    `php // app/Providers/RouteServiceProvider.php (estratto) public function boot(): void { parent::boot(); Route::middleware('web')->group(function () { require base_path('routes/auth.php'); require base_path('routes/admin.php'); }); } `

    4.2 Rotte auth (login esterno + role select)

    `php // routes/auth.php (estratto) Route::middleware('guest')->group(function () { Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login'); Route::post('/login', [LoginController::class, 'login'])->name('login.post'); Route::get('/password/forgot', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request'); Route::post('/password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email'); Route::get('/password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset'); Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update'); }); Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth'); Route::middleware('auth')->group(function () { Route::get('/auth/select-role', [RoleSelectorController::class,'show'])->name('auth.role.select'); Route::post('/auth/set-role', [RoleSelectorController::class,'set'])->name('auth.role.set'); }); `

    4.3 Rotte area Admin (contenitore unico)

    `php // routes/admin.php (estratto) Route::middleware(['auth','active.role'])->prefix('admin')->name('admin.')->group(function () { Route::get('/', [\App\Http\Controllers\Admin\Dashboard\DashboardController::class,'index'])->name('dashboard'); Route::prefix('i40')->name('i40.')->group(function() { Route::get('/', [\App\Http\Controllers\Admin\I40\HomeController::class,'index'])->name('home'); Route::get('/machines/connected', [\App\Http\Controllers\Admin\I40\Machines\MachinesController::class,'connected'])->name('machines.connected'); }); Route::prefix('systems')->name('systems.')->middleware('role:super-admin')->group(function () { Route::view('/', 'admin.systems.index')->name('home'); Route::prefix('menu')->name('menu.')->group(function () { Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('index'); Route::get('/create', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'create'])->name('create'); Route::post('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'store'])->name('store'); Route::get('/{item}/edit', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'edit'])->name('edit'); Route::put('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'update'])->name('update'); Route::delete('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'destroy'])->name('destroy'); Route::post('/reorder', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'reorder'])->name('reorder'); }); }); }); `

    4.4 Sicurezza & middleware

    `php // app/Http/Kernel.php (estratto) protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'active.role' => \App\Http\Middleware\EnsureActiveRole::class, 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, 'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class, 'roles_or_permissions' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class, ]; ` `php // app/Providers/AuthServiceProvider.php (estratto) Gate::before(function ($user, $ability) { return $user->hasRole('super-admin') ? true : null; }); `

    ---

    5) Esempio modulo (I4.0 → Macchine → Elenco collegate)

    `blade {{-- resources/views/admin/i40/machines/connected.blade.php --}} @extends('layouts.admin') @section('title','Elenco macchine collegate') @section('content') <div class="bg-white border rounded shadow p-4"> <div class="mb-3 text-sm text-gray-500">Tabella in tempo reale (placeholder)</div> <table class="min-w-full"> <thead> <tr class="text-left text-sm text-gray-600"> <th class="p-2">Macchina</th> <th class="p-2">Protocollo</th> <th class="p-2">Stato</th> <th class="p-2">Last seen</th> </tr> </thead> <tbody> <tr class="border-t"> <td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td> </tr> </tbody> </table> </div> @endsection `

    ---

    6) Seed essenziale (menù L1 + Servizio + Configurazione menù)

    `php $admin = \App\Models\Menu::firstOrCreate(['name'=>'admin_main'], ['description'=>'Menu principale admin']);

    \App\Models\MenuItem::firstOrCreate([ 'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Dashboard' ],[ 'route_name'=>'admin.dashboard','icon'=>'lucide-home','order_index'=>1 ]);

    $ind40 = \App\Models\MenuItem::firstOrCreate([ 'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Industria 4.0' ],[ 'icon'=>'lucide-cpu','order_index'=>2 ]);

    $report = \App\Models\MenuItem::firstOrCreate([ 'menu_id'=>$admin->id,'parent_id'=>$ind40->id,'label'=>'Report' ],[ 'order_index'=>1 ]);

    $macchine = \App\Models\MenuItem::firstOrCreate([ 'menu_id'=>$admin->id,'parent_id'=>$report->id,'label'=>'Macchine' ],[ 'order_index'=>1 ]);

    \App\Models\MenuItem::firstOrCreate([ 'menu_id'=>$admin->id,'parent_id'=>$macchine->id,'label'=>'Elenco macchine collegate' ],[ 'route_name'=>'admin.i40.machines.connected', 'order_index'=>1, 'required_roles'=>json_encode(['admin','maintenance','super-admin']) // aggiungi 'operator' se vuoi ]);

    $service = \App\Models\MenuItem::firstOrCreate([ 'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Servizio' ],[ 'icon'=>'lucide-wrench','order_index'=>100, 'required_roles'=>json_encode(['super-admin']) ]);

    \App\Models\MenuItem::firstOrCreate([ 'menu_id'=>$admin->id,'parent_id'=>$service->id,'label'=>'Configurazione menù' ],[ 'route_name'=>'admin.systems.menu.index', 'order_index'=>1, 'required_roles'=>json_encode(['super-admin']) ]); ``

    ---

    7) Criteri di accettazione

  • Login esterno con layout 1/3–2/3, link “forgot”.
  • Dopo login: redirect a Admin con Topbar (logo+L1 a sinistra, profilo a destra).
  • Sidebar con 3 livelli coerenti con la voce L1 attiva.
  • Breadcrumb + Titolo modulo in header del corpo centrale.
  • Servizio (super-admin) presente; Configurazione menù come prima voce (CRUD operativo).
  • I4.0 → Report → Macchine → Elenco macchine collegate presente (placeholder).
  • ---

    8) Note operative

  • Il breadcrumb deriva dal ramo del menù (no hardcode).
  • Il filtro RBAC si applica a Topbar e, opzionalmente, alla Sidebar.
  • Logo consigliato: quadrato (32–40px), reso circolare via CSS.
  • Il layout è Blade-first (semplice da personalizzare), pronto a Livewire/Inertia in futuro.

Analisi Codice

Blocco 1
app/Http/Controllers/Admin/
  Dashboard/DashboardController.php
  I40/HomeController.php
  I40/Machines/MachinesController.php
  Systems/               # “Servizio” (solo super-admin)
    Menu/MenuController.php
    Users/UsersController.php  (placeholder)
    Roles/RolesController.php  (placeholder)

app/Services/MenuService.php
app/Http/Middleware/EnsureActiveRole.php
app/Policies/MenuPolicy.php (opzionale)
app/Models/Menu.php
app/Models/MenuItem.php

resources/views/layouts/admin.blade.php
resources/views/layouts/auth.blade.php      # login esterno (1/3–2/3)
resources/views/auth/{login,select-role,forgot,reset}.blade.php

resources/views/admin/dashboard/index.blade.php
resources/views/admin/i40/home.blade.php
resources/views/admin/i40/machines/connected.blade.php
resources/views/admin/systems/index.blade.php
resources/views/admin/systems/menu/{index,create,edit}.blade.php

routes/auth.php
routes/admin.php
Blocco 2 blade
{{-- resources/views/layouts/auth.blade.php --}}
<!doctype html>
<html lang="it">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
  <title>@yield('title','Accedi | sartUP')</title>
  @vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-50">
  <div class="mx-auto max-w-6xl min-h-screen grid grid-cols-3">
    {{-- Colonna sinistra 1/3: branding --}}
    <aside class="col-span-1 bg-white border-r p-8 flex flex-col justify-center">
      <div class="flex items-center gap-3 mb-6">
        <img src="/images/logo.png" alt="sartUP" class="h-12 w-12 rounded-full object-cover">
        <div>
          <h1 class="text-xl font-semibold">sartUP</h1>
          <p class="text-sm text-gray-500">Gestionale modulare per confezione</p>
        </div>
      </div>
      <div class="text-gray-600 text-sm leading-relaxed">
        <p>Accedi per gestire modelli, tempi & metodi, integrazioni Industria 4.0 e reportistica.</p>
      </div>
      {{-- <img src="/images/login-side.jpg" class="mt-8 rounded-xl shadow" alt=""> --}}
    </aside>

    {{-- Colonna destra 2/3: form --}}
    <main class="col-span-2 flex items-center justify-center p-8">
      <div class="w-full max-w-md bg-white rounded-xl shadow p-6">
        @yield('content')
      </div>
    </main>
  </div>
</body>
</html>
Blocco 3 blade
{{-- resources/views/auth/login.blade.php --}}
@extends('layouts.auth')
@section('title','Accedi')
@section('content')
  <h2 class="text-lg font-semibold mb-4">Accedi</h2>
  <form method="POST" action="{{ route('login.post') }}" class="space-y-4">
    @csrf
    <div>
      <label class="block text-sm mb-1">Email</label>
      <input type="email" name="email" value="{{ old('email') }}" required autofocus class="w-full border rounded p-2">
      @error('email') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
    </div>
    <div>
      <label class="block text-sm mb-1">Password</label>
      <input type="password" name="password" required class="w-full border rounded p-2">
      @error('password') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
    </div>
    <div class="flex items-center justify-between">
      <label class="inline-flex items-center gap-2 text-sm">
        <input type="checkbox" name="remember" class="rounded"> Ricordami
      </label>
      <a class="text-sm underline" href="{{ route('password.request') }}">Hai dimenticato la password?</a>
    </div>
    <button class="w-full py-2 rounded bg-blue-600 text-white font-medium">Entra</button>
  </form>
@endsection
Blocco 4 blade
{{-- resources/views/layouts/admin.blade.php --}}
@php
  /** @var \App\Services\MenuService $ms */
  $ms = app(\App\Services\MenuService::class);
  $menuL1 = $ms->forUserMenu('admin_main', auth()->user());        // topbar
  $active = $ms->currentBranchByRoute('admin_main');               // item corrente + ancestors
  $sidebarTree = $ms->childrenForTopLevel($active['top'] ?? $menuL1[0] ?? null); // L2/L3/L4
@endphp
<!doctype html>
<html lang="it">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
  <title>@yield('title','sartUP Admin')</title>
  @vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-50">
  {{-- TOPBAR --}}
  <header class="h-14 shadow flex items-center px-4 bg-white">
    <div class="flex items-center gap-3 mr-6">
      <img src="/images/logo.png" class="h-8 w-8 rounded-full object-cover" alt="logo">
    </div>
    <nav class="flex gap-4">
      @foreach($menuL1 as $item)
        <a href="{{ $item['route_name'] ? route($item['route_name']) : ($item['url'] ?? '#') }}"
           class="font-medium {{ request()->routeIs(($item['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}">
          {{ $item['label'] }}
        </a>
      @endforeach
      @role('super-admin')
        <a href="{{ route('admin.systems.menu.index') }}"
           class="font-medium {{ request()->is('admin/systems*') ? 'text-blue-600' : '' }}">Servizio</a>
      @endrole
    </nav>
    <div class="ml-auto flex items-center gap-3">
      @if(session('active_role'))
        <span class="text-xs px-2 py-1 bg-gray-200 rounded">Ruolo: {{ session('active_role') }}</span>
      @endif
      <div class="relative">
        <details>
          <summary class="list-none flex items-center gap-2 cursor-pointer">
            <div class="h-8 w-8 rounded-full bg-gray-200 flex items-center justify-center">
              {{ strtoupper(substr(auth()->user()->name ?? 'U',0,1)) }}
            </div>
            <span class="text-sm">{{ auth()->user()->name }}</span>
          </summary>
          <div class="absolute right-0 mt-2 bg-white border rounded shadow min-w-[200px]">
            <a class="block px-3 py-2 hover:bg-gray-50" href="{{ route('auth.role.select') }}">Cambia ruolo</a>
            <a class="block px-3 py-2 hover:bg-gray-50" href="#">Profilo</a>
            <form method="POST" action="{{ route('logout') }}" class="border-t">
              @csrf
              <button class="w-full text-left px-3 py-2 hover:bg-gray-50">Logout</button>
            </form>
          </div>
        </details>
      </div>
    </div>
  </header>

  <div class="flex">
    {{-- SIDEBAR: L2/L3/L4 --}}
    <aside class="w-72 bg-white border-r min-h-[calc(100vh-3.5rem)] p-4">
      @if(!empty($sidebarTree))
        @foreach($sidebarTree as $l2)
          <div class="mb-3">
            <div class="text-sm font-semibold mb-1">{{ $l2['label'] }}</div>
            @if(!empty($l2['children']))
              <ul class="ml-3 space-y-1">
                @foreach($l2['children'] as $l3)
                  <li>
                    <a class="block text-sm {{ request()->routeIs(($l3['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}"
                       href="{{ $l3['route_name'] ? route($l3['route_name']) : ($l3['url'] ?? '#') }}">
                      {{ $l3['label'] }}
                    </a>
                    @if(!empty($l3['children']))
                      <ul class="ml-4 mt-1 space-y-1">
                        @foreach($l3['children'] as $l4)
                          <li>
                            <a class="block text-sm {{ request()->routeIs(($l4['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}"
                               href="{{ $l4['route_name'] ? route($l4['route_name']) : ($l4['url'] ?? '#') }}">
                              {{ $l4['label'] }}
                            </a>
                          </li>
                        @endforeach
                      </ul>
                    @endif
                  </li>
                @endforeach
              </ul>
            @endif
          </div>
        @endforeach
      @endif
    </aside>

    {{-- MAIN: breadcrumb + titolo + body --}}
    <main class="flex-1 p-6">
      <div class="mb-4">
        <nav class="text-sm text-gray-500">
          @if(!empty($active['trail']))
            @foreach($active['trail'] as $i => $crumb)
              @if($i>0) <span class="mx-1">/</span> @endif
              @if($crumb['route_name'])
                <a class="hover:underline" href="{{ route($crumb['route_name']) }}">{{ $crumb['label'] }}</a>
              @else
                <span>{{ $crumb['label'] }}</span>
              @endif
            @endforeach
          @endif
        </nav>
        <h1 class="text-xl font-semibold mt-1">@yield('title','Modulo')</h1>
      </div>
      @yield('content')
    </main>
  </div>
</body>
</html>
Blocco 5 php
// App/Services/MenuService.php (estratti/aggiunte)
public function currentBranchByRoute(string $menuName = 'admin_main'): array
{
    $menu = Menu::where('name',$menuName)->first();
    if (!$menu) return ['trail'=>[], 'top'=>null];
    $items = $menu->items()->get();
    $byId = $items->keyBy('id');
    $current = $items->first(function($i){ return $i->route_name && request()->routeIs($i->route_name.'*'); });
    if (!$current) return ['trail'=>[], 'top'=>null];

    $trail = [];
    while ($current) {
        $trail[] = ['label'=>$current->label,'route_name'=>$current->route_name,'id'=>$current->id,'parent_id'=>$current->parent_id];
        $current = $current->parent_id ? $byId->get($current->parent_id) : null;
    }
    $trail = array_reverse($trail);
    $top = $trail[0] ?? null;
    return ['trail'=>$trail, 'top'=>$top];
}

public function childrenForTopLevel(?array $top): array
{
    if (!$top) return [];
    $menu = Menu::where('name','admin_main')->first();
    if (!$menu) return [];
    $items = $menu->items()->orderBy('order_index')->get()->groupBy('parent_id');
    $build = function($parentId) use (&$build,$items) {
        return ($items[$parentId] ?? collect())->map(fn($i)=>
            ['id'=>$i->id,'label'=>$i->label,'route_name'=>$i->route_name,'url'=>$i->url,'children'=>$build($i->id)->values()->all()]
        );
    };
    return $build($top['id'] ?? null)->values()->all();
}
Blocco 6 php
// app/Providers/RouteServiceProvider.php (estratto)
public function boot(): void
{
    parent::boot();
    Route::middleware('web')->group(function () {
        require base_path('routes/auth.php');
        require base_path('routes/admin.php');
    });
}
Blocco 7 php
// routes/auth.php (estratto)
Route::middleware('guest')->group(function () {
  Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
  Route::post('/login', [LoginController::class, 'login'])->name('login.post');
  Route::get('/password/forgot', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
  Route::post('/password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
  Route::get('/password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
  Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update');
});
Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');
Route::middleware('auth')->group(function () {
  Route::get('/auth/select-role', [RoleSelectorController::class,'show'])->name('auth.role.select');
  Route::post('/auth/set-role', [RoleSelectorController::class,'set'])->name('auth.role.set');
});
Blocco 8 php
// routes/admin.php (estratto)
Route::middleware(['auth','active.role'])->prefix('admin')->name('admin.')->group(function () {
  Route::get('/', [\App\Http\Controllers\Admin\Dashboard\DashboardController::class,'index'])->name('dashboard');
  Route::prefix('i40')->name('i40.')->group(function() {
    Route::get('/', [\App\Http\Controllers\Admin\I40\HomeController::class,'index'])->name('home');
    Route::get('/machines/connected', [\App\Http\Controllers\Admin\I40\Machines\MachinesController::class,'connected'])->name('machines.connected');
  });
  Route::prefix('systems')->name('systems.')->middleware('role:super-admin')->group(function () {
    Route::view('/', 'admin.systems.index')->name('home');
    Route::prefix('menu')->name('menu.')->group(function () {
      Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('index');
      Route::get('/create', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'create'])->name('create');
      Route::post('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'store'])->name('store');
      Route::get('/{item}/edit', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'edit'])->name('edit');
      Route::put('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'update'])->name('update');
      Route::delete('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'destroy'])->name('destroy');
      Route::post('/reorder', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'reorder'])->name('reorder');
    });
  });
});
Blocco 9 php
// app/Http/Kernel.php (estratto)
protected $routeMiddleware = [
  'auth'        => \App\Http\Middleware\Authenticate::class,
  'guest'       => \App\Http\Middleware\RedirectIfAuthenticated::class,
  'verified'    => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
  'active.role' => \App\Http\Middleware\EnsureActiveRole::class,
  'role'        => \Spatie\Permission\Middlewares\RoleMiddleware::class,
  'permission'  => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
  'roles_or_permissions' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class,
];
Blocco 10 php
// app/Providers/AuthServiceProvider.php (estratto)
Gate::before(function ($user, $ability) {
  return $user->hasRole('super-admin') ? true : null;
});
Blocco 11 blade
{{-- resources/views/admin/i40/machines/connected.blade.php --}}
@extends('layouts.admin')
@section('title','Elenco macchine collegate')
@section('content')
  <div class="bg-white border rounded shadow p-4">
    <div class="mb-3 text-sm text-gray-500">Tabella in tempo reale (placeholder)</div>
    <table class="min-w-full">
      <thead>
        <tr class="text-left text-sm text-gray-600">
          <th class="p-2">Macchina</th>
          <th class="p-2">Protocollo</th>
          <th class="p-2">Stato</th>
          <th class="p-2">Last seen</th>
        </tr>
      </thead>
      <tbody>
        <tr class="border-t">
          <td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td>
        </tr>
      </tbody>
    </table>
  </div>
@endsection
Blocco 12 php
$admin = \App\Models\Menu::firstOrCreate(['name'=>'admin_main'], ['description'=>'Menu principale admin']);

\App\Models\MenuItem::firstOrCreate([
  'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Dashboard'
],[
  'route_name'=>'admin.dashboard','icon'=>'lucide-home','order_index'=>1
]);

$ind40 = \App\Models\MenuItem::firstOrCreate([
  'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Industria 4.0'
],[ 'icon'=>'lucide-cpu','order_index'=>2 ]);

$report = \App\Models\MenuItem::firstOrCreate([
  'menu_id'=>$admin->id,'parent_id'=>$ind40->id,'label'=>'Report'
],[ 'order_index'=>1 ]);

$macchine = \App\Models\MenuItem::firstOrCreate([
  'menu_id'=>$admin->id,'parent_id'=>$report->id,'label'=>'Macchine'
],[ 'order_index'=>1 ]);

\App\Models\MenuItem::firstOrCreate([
  'menu_id'=>$admin->id,'parent_id'=>$macchine->id,'label'=>'Elenco macchine collegate'
],[
  'route_name'=>'admin.i40.machines.connected',
  'order_index'=>1,
  'required_roles'=>json_encode(['admin','maintenance','super-admin']) // aggiungi 'operator' se vuoi
]);

$service = \App\Models\MenuItem::firstOrCreate([
  'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Servizio'
],[
  'icon'=>'lucide-wrench','order_index'=>100,
  'required_roles'=>json_encode(['super-admin'])
]);

\App\Models\MenuItem::firstOrCreate([
  'menu_id'=>$admin->id,'parent_id'=>$service->id,'label'=>'Configurazione menù'
],[
  'route_name'=>'admin.systems.menu.index',
  'order_index'=>1,
  'required_roles'=>json_encode(['super-admin'])
]);